<?php
/**
 * OpenID 2.0 Authentication for PHP
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2006-2007 Pádraic Brady <padraic.brady@yahoo.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * The name of the author may not be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category Authentication
 * @package  OpenID
 * @author   Pádraic Brady <padraic.brady@yahoo.com>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @link     http://pearopenid.googlecode.com/
 */

/** Validate */
require_once 'Validate.php';

/** OpenID */
require_once 'OpenID.php';

/** OpenID_Association */
require_once 'OpenID/Association.php';

/** OpenID_Redirect_Authorisation */
require_once 'OpenID/Redirect/Authorisation.php';

/** OpenID_Response_Authorisation */
require_once 'OpenID/Response/Authorisation.php';

/**
 * OpenID Authentication 2.0 implementation in PHP5.
 *
 * @todo Remove unneeded XRI parsing checks in favour of a simple _isXRI boolean
 * @category Authentication
 * @package  OpenID
 * @author   Pádraic Brady <padraic.brady@yahoo.com>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @link     http://pearopenid.googlecode.com/
 */
class OpenID_Consumer extends OpenID
{

    /**
     * The URL/XRI supplied by the end user as an Identifier but which has not
     * yet been determined as a valid Claimed Identifier.
     *
     * @var string
     */
    private $_userIdentifier = null;

    /**
     * The URL/XRI supplied by the end user which they are claiming as an OpenID
     *
     * @var string
     */
    private $_claimedIdentifier = null;

    /**
     * The final OP-Local Identifier the OP has recognised.
     * This is typically the Identifier any User aliases are pointing to.
     *
     * @var string
     */
    private $_localIdentifier = null;

    /**
     * An object representing a method of storing associations
     *
     * @var OpenID_Store_Interface
     */
    private $_store = null;

    /**
     * Boolean value indicating whether we should use stateless mode, i.e.
     * we will not store any data between requests via an
     * OpenID_Store_Interface strategy or the Session.
     *
     * @var boolean
     */
    protected $statelessMode = false;

    /**
     * Boolean value indicating whether we should use encryption (i.e Diffie-
     * Hellman Key Exchange) when establishing a shared secret. Otherwise the
     * key is sent in plain text without any DH style agreement.
     *
     * @var boolean
     */
    protected $encryption = true;

    /**
     * An array representing the SESSION
     *
     * @var array
     */
    private $_session = null;

    /**
     * Request URI for the Identity Provider
     *
     * @var string
     */
    private $_opEndpoint = null;

    /**
     * Association request type
     */
    private $_associationRequest = null;

    /**
     * List of Service types recovered during discovery of the OP Server
     */
    private $_serviceTypes = array();

    /**
     * Identifier characters for an XRI name
     *
     * @var array
     */
    private $_xriIdentifiers = array('=', '$', '!', '@', '+');

    /**
     * Set storage container and session (not used yet)
     *
     * @param OpenID_Store_Interface $store   Instnace of OpenID_Store_Interface
     * @param array                  $session $_SESSION superglobal
     *
     * @return void
     */
    public function __construct(OpenID_Store_Interface $store, array $session = null)
    {
        $this->_store   = $store;
        $this->_session = $session; // not used yet
    }

    /**
     * Commence Association/Authorisation; the act of establishing a shared
     * secret key for encrypting subsequent communication.
     * The Claimed Identifier (user's URL) is used to locate the Identity
     * Provider server with which to associate using an OpenID 1.1 backwards
     * compatible Yadis Protocol.
     * If cached, the cached association data is returned preferentially.
     *
     * The second parameter may be set to false to strip Authentication
     * redirects of identity data. This is useful if the request is being
     * piggy backed by Extension parameters which are unrelated to any
     * individual Identity.
     *
     * @param string  $userIdentifier   openid_identifier
     * @param boolean $passIdentityData Whether to pass identity data or not. 
     *                                  Defaults to true
     *
     * @return OpenID_KVContainer
     */
    public function start($userIdentifier, $passIdentityData = true)
    {
        $this->setUserIdentifier($userIdentifier);
        $association = new OpenID_Association($this);
        $assocData   = $association->getAssociation();

        if (isset($assocData->error)) {
            throw new OpenID_Exception(
                'unable to association with the user\'s OP Provider: "'
                . $assocData->error . '"'
            );
        }
        $authoriseRedirect = new OpenID_Redirect_Authorisation(
            $assocData,
            $this,
            $passIdentityData
        );
        return $authoriseRedirect;
    }

    /**
     * Finish authorisation by processing an authorisation response from
     * the server and returning a relevant success/other return object
     *
     * @param array $params Typically just $_GET or a validated array
     *
     * @return OpenID_Response_Authorisation
     */
    public function finish(array $params)
    {
        return new OpenID_Response_Authorisation($params);
    }

    /**
     * Set the user's URL (User Identifier) to be used
     *
     * @param string $userIdentifier User Identifier
     *
     * @throws OpenID_Exception on invalid uri ($useridentifier)
     * @return void
     */
    public function setUserIdentifier($userIdentifier)
    {
        if ($this->isXri($userIdentifier)) {
            $this->_userIdentifier = $userIdentifier;
            return;
        }
        if (strpos($userIdentifier, "://") === false) {
            $userIdentifier = rtrim("http://" . $userIdentifier, '\\/') . '/';
        }
        if (!Validate::uri($userIdentifier)) {
            throw new OpenID_Exception(
                'the User Identifier is not a valid URI or an XRI'
            );
        }
        $this->_userIdentifier = $userIdentifier;
    }

    /**
     * Getter for the User Identifier
     *
     * @return string
     */
    public function getUserIdentifier()
    {
        return $this->_userIdentifier;
    }

    /**
     * Set the user's URL (Claimed Identifier) to be used
     *
     * @param string $claimedIdentifier Claimed Identifier
     *
     * @throws OpenID_Exception on invalid claimed itentifier
     * @return void
     */
    public function setClaimedIdentifier($claimedIdentifier)
    {
        if ($this->isXri($claimedIdentifier)) {
            $this->_claimedIdentifier = $claimedIdentifier;
            return;
        }
        // otherwise it must be a URI
        if (strpos($claimedIdentifier, "://") === false) {
            $claimedIdentifier = rtrim("http://" . $claimedIdentifier, '\\/') . '/';
        }
        if (!Validate::uri($claimedIdentifier)) {
            throw new OpenID_Exception(
                'the Claimed Identifier is not a valid URI or an XRI'
            );
        }
        $this->_claimedIdentifier = $claimedIdentifier;
    }

    /**
     * Getter for the Claimed Identifier
     *
     * @return string
     */
    public function getClaimedIdentifier()
    {
        return $this->_claimedIdentifier;
    }

    /**
     * This is the Identifier any user's alias corresponds to which
     * is determined during discovery.
     *
     * @param string $localIdentifier Local Identifier
     *
     * @return void
     */
    public function setLocalIdentifier($localIdentifier)
    {
        // if an XRI we should have located the associated Canonical ID by now
        if ($this->isCanonicalId($localIdentifier)) {
            $this->_localIdentifier = $localIdentifier;
            return;
        }
        // otherwise it must be a URI
        if (strpos($localIdentifier, "://") === false) {
            $localIdentifier = rtrim("http://" . $localIdentifier, '\\/') . '/';
        }
        if (!Validate::uri($localIdentifier)) {
            throw new OpenID_Exception(
                'the Local OP Identifier is not a valid URI or an XRI Canonical ID'
            );
        }
        $this->_localIdentifier = $localIdentifier;
    }

    /**
     * Getter for the Local Identifier
     *
     * @throws OpenID_Exception if {$this->_localIdentifier} is not set
     * @return string
     */
    public function getLocalIdentifier()
    {
        if (!isset($this->_localIdentifier)) {
            throw new OpenID_Exception(
                'The Local Identifier, an OpenID Provider recognised Identity,' .
                ' has not yet been set'
            );
        }
        return $this->_localIdentifier;
    }

    /**
     * Setter for the Identity Provider Request URI
     *
     * @param string $requestUri Identity Provider Request URI
     *
     * @throws OpenID_Exception if invalid Identity Provider
     * @return void
     */
    public function setOpEndpoint($requestUri)
    {
        $requestUri = trim($requestUri, '\\/');
        if (!Validate::uri($requestUri)) {
            throw new OpenID_Exception(
                'The Identity Provider URI discovered is not a valid URI'
            );
        }
        $this->_opEndpoint = $requestUri;
    }

    /**
     * Getter for the Identity Provider Request URI
     *
     * @see $this->_opEndpoint
     * @return string
     */
    public function getOpEndpoint()
    {
        return $this->_opEndpoint;
    }

    /**
     * Fetch the current OpenID_Store_Interface object
     *
     * @see $this->store
     * @return OpenID_Store_Interface
     */
    public function getStore()
    {
        return $this->_store;
    }

    /**
     * If Yadis discovery was successful, this holds an array
     * of the relevant Services discovered as SimpleXML objects
     *
     * @param array $services Array of services from Yadis Discovery
     *
     * @return void
     */
    public function setServiceTypes(array $services)
    {
        $this->_services = $services;
    }

    /**
     * Fetch the array of Service SimpleXML objects found during
     * Yadis Service Discovery
     *
     * @see $this->_services
     * @return array
     */
    public function getServiceTypes()
    {
        return $this->_services;
    }

    /**
     * Get the last Association request object
     *
     * @see $this->_associationRequest
     * @return OpenID_Request_Association
     */
    public function getAssociationRequest()
    {
        return $this->_associationRequest;
    }

    /**
     * Simple Session wrapper until something more robust turns up
     *
     * @param array $data Array of session data to set
     *
     * @return void
     */
    public function addSessionData(array $data)
    {
        foreach ($data as $key => $val) {
            $_SESSION[OpenID::OPENID_PHP_SESSION_NAMESPACE][$key] = $val;
        }
    }

    /**
     * Simple Session wrapper until something more robust turns up
     *
     * @param array $data session data array
     *
     * @return string
     */
    public function getSessionData(array $data)
    {
        return $_SESSION[OpenID::OPENID_PHP_SESSION_NAMESPACE];
    }

    /**
     * Set whether we should operate in Stateless Mode; no data will
     * be stored to the PHP Session or a backend Store solution. And
     * the initial MAC key will be sent in the clear.
     *
     * @param boolean $enable true to enable stateless mode, false do disable
     *
     * @return void
     */
    public function useStatelessMode($enable)
    {
        $this->statelessMode = (bool) $enable;
    }

    /**
     * Returns a boolean value indicating whether Stateless Mode is enabled
     *
     * @return boolean
     */
    public function getStatelessMode()
    {
        return $this->statelessMode;
    }

    /**
     * Set whether we should operate under an encrypted mode. Basically unless
     * You are using SSL don't even think of setting this to false! Unencrypted
     * Associations send the HMAC key in plain text without using the Diffie
     * Hellman Key Exchange protocol.
     *
     * @param boolean $enable true to enable encryption, false otherwise
     *
     * @return void
     */
    public function useEncryption($enable)
    {
        $this->encryption = (bool) $enable;
    }

    /**
     * Returns a boolean value indicating whether Encryption is enabled
     *
     * @see $this->encryption
     * @return boolean
     */
    public function getEncryption()
    {
        return $this->encryption;
    }

    /**
     * Establish whether a claimed identifier is an XRI
     *
     * @param string $identifier Claimed Identifier
     *
     * @return bool true on success, false on failure
     */
    protected function isXri($identifier)
    {
        if (strpos($identifier, 'xri://') === 0 
            || in_array($identifier[0], $this->_xriIdentifiers)) {
            return true;
        }
        return false;
    }

    /**
     * Establish whether a claimed identifier is an XRI Canonical ID
     *
     * @param string &$identifier Claimed Identifier
     *
     * @todo Some servers will prefix this with the "xri://" protocol
     * @return bool
     */
    protected function isCanonicalId(&$identifier)
    {
        $r = preg_match("/^!=[A-Z0-9]{4}\.[A-Z0-9]{4}\.[A-Z0-9]{4}\.[A-Z0-9]{4}$/",
            $identifier);
        return (bool) $r;
    }

}
?>
